ApexでGoのLambdaを作成しようとしたら少しハマった
はじめに
Lambdaを作成するご用事があったので、goで作ってみることにしました。デプロイにapexを使ってみたのですが、少しハマったので、原因と解決方法をまとめておきます。
TL;DR
- apexでgoを使うには
"go get -v -t -d ./... && GOOS=linux GOARCH=amd64 go build -o main main.go"
をフックに追加が必要 function.json
にフックがあるとproject.json
のフックは呼ばれない- 別法として
"runtime": "golang"
を入れるとビルドしてくれるようになるが、フックがあると効果が無くなる
使用環境
$ sw_vers ProductName: Mac OS X ProductVersion: 10.12.6 BuildVersion: 16G1510
apex
Lambdaを作成するときにマネージメントコンソールで作成するのは少々面倒ですが、apexというツールを使うとコマンドラインで作業を進められるので幸せになれそうです。
まずは手始めに、apexのgithubリポジトリにある、apex/_examples/go
を試してみました。
インストール
apexコマンドを下記のようにインストールします。
$ curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh
バージョンは下記でした。
$ apex version Apex version 1.0.0-rc2
サンプルの取得
リポジトリを丸ごとcloneしておきます。
$ git clone https://github.com/apex/apex
goのサンプルは、apex/_examples/go
にあります。
AWS-CLIとクレデンシャルの用意
aws-cliが使える環境を用意しておきます。
IAMポリシーの作成
サンプルを使うためには、project.json
にroleを設定する必要がありまし。公式サイトに記載されているように、マネージメントコンソールからroleを作成したうえで、下記の内容のポリシーをアタッチしておきます。
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "iam:CreateRole", "iam:CreatePolicy", "iam:AttachRolePolicy", "iam:PassRole", "lambda:GetFunction", "lambda:ListFunctions", "lambda:CreateFunction", "lambda:DeleteFunction", "lambda:InvokeFunction", "lambda:GetFunctionConfiguration", "lambda:UpdateFunctionConfiguration", "lambda:UpdateFunctionCode", "lambda:CreateAlias", "lambda:UpdateAlias", "lambda:GetAlias", "lambda:ListAliases", "lambda:ListVersionsByFunction", "logs:FilterLogEvents", "cloudwatch:GetMetricStatistics" ], "Effect": "Allow", "Resource": "*" } ] }
roleのarnをproject.json
に設定します。
{ "name": "go", "description": "Go example project", "role": "arn:aws:iam::XXXXXXXXXXXX:role/api-function" }
デプロイ
あとは、コマンドラインからこんな感じでデプロイするだけです。
$ cd apex/_examples/go $ apex deploy • creating function env= function=simple • created alias current env= function=simple version=1 • function created env= function=simple name=go_simple version=1
マネージメントコンソールで確認すると、lambdaがそれらしくできていました。
ところが...
デプロイしたlambdaは、apex invokeコマンドで実行可能となるはずです。ところが、いざ実行してみようとすると、こんなエラーが発生してしまいました。
$ apex invoke simple <event.json ⨯ Error: function response: fork/exec /var/task/main: no such file or directory
最初は、apexが何をやっているのかわかっていなかったので、原因を調べるのに手こずってしまいました。apex build
コマンドでzipファイルを作成してみると、中身が.go
のソースファイルしか入っていません。
$ apex build simple > main.zip $ unzip -Lv main.zip Archive: main.zip Length Method Size Cmpr Date Time CRC-32 Name ("^" ==> case -------- ------ ------- ---- ---------- ----- -------- ---- conversion) 280 Defl:N 197 30% 01-01-1970 09:00 c43fdcff main.go -------- ------- --- ------- 280 197 30% 1 file
このzipがアップロードされたとしても、lambdaでは実行しようもありません。エラーの原因はapexがgoのコンパイルをしてくれておらず、バイナリが入っていないということでした。
対応:フックを追加する
これを修正するため、project.json
に下記を追記します。これはちゃんと公式サイトに記載されています。しかしサンプルapex/_example/go
には設定されていませんでした。
"hooks": { "build": "GOOS=linux GOARCH=amd64 go build -o main main.go", "clean": "rm -f main" }
ところが、これをやっても正常にビルドが行われませんでした。というのも、このサンプルのfunction.json
には外部モジュールを引っ張ってくるための別のフックが記述されており、それが優先された結果project.json
のフックが無視されてしまっていたようです。
"hooks":{ "build": "go get -v -t -d ./..." }
なので正解は、上記二つを合わせた下記のフックをfunction.json
もしくはproject.json
のいずれかに設定することのようです。今回は、function.json
のみにフックを作成することにしました。
"hooks": { "build": "go get -v -t -d ./... && GOOS=linux GOARCH=amd64 go build -o main main.go", "clean": "rm -f main" }
.apexignore
アップロードするzipファイルに必要なのはバイナリだけで、ソースファイルは必要ありません。そのため、.apexignore
ファイルを下記を内容で作成します。
*.go
以上の作業により、必要十分な内容のzipファイルがapex build
で作成されるようになりました。mainのバイナリサイズはおよそ860kByteとなっています。
$ apex build simple > main.zip && unzip -Lv main.zip Archive: xxx.zip Length Method Size Cmpr Date Time CRC-32 Name ("^" ==> case -------- ------ ------- ---- ---------- ----- -------- ---- conversion) 8610976 Defl:N 3052041 65% 01-01-1970 09:00 8eace7d4 main -------- ------- --- ------- 8610976 3052041 65% 1 file
実行
apex invoke
コマンドで実行してみます。入力にあらかじめ用意されているevent.json
を指定します。
$ apex invoke simple <event.json "Hello Tobi, you are a Ferret"
無事成功しました!
--logs
オプションを付けると、ログも同時に確認することができます。
$ apex invoke simple --logs <event.json START RequestId: 4b19b319-9b04-11e8-bae2-df4f63b4fe20 Version: 15 END RequestId: 4b19b319-9b04-11e8-bae2-df4f63b4fe20 REPORT RequestId: 4b19b319-9b04-11e8-bae2-df4f63b4fe20 Duration: 0.42 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 22 MB "Hello Tobi, you are a Ferret"
実行時間やメモリ量など参考になる情報が得られます。
もう一つの方法
上の例ではフックを追加する方法を示しましたが、実は別にもう一つ方法があることに気づきました。その方法とは、function.json
もしくはproject.json
に、"runtime": "golang"
を追加するのです。そうすると、ちゃんとApex組み込みのgo向けビルドルールが適用され、フックがなくてもちゃんとバイナリが生成されるようになります。
{ "description": "Example", "runtime": "golang" }
これで解決かと思いきや、実はこの方法では外部パッケージのダウンロードをしてくれないのです。元のように、フックで外部パッケージをダウンロードのコマンドを入れてしまうと"runtime": "golang"
の効果が失われ、またしてもバイナリが作られないのです。外部パッケージのロードは一度行えば、以後は不要となるのですが、もやっとすることは否めません。
結論としては、フックで明示的に、ダウンロードとビルドを共に行うのが最も汎用性があるようです。
まとめ
最初は何が起きているのかわからず、かなり回り道をしてしまいました。おかげでapexの理解が進んだようにも思います。デプロイ時にエラーが生じなかったので、つい油断していた気もします。私のハマった数時間が、どなたかのお役に立てば幸いです。
参考
- http://apex.run/
- https://github.com/apex/apex/blob/master/docs/hooks.md
- https://dev.classmethod.jp/cloud/aws/aws-lambda-supports-go/
- https://dev.classmethod.jp/cloud/aws/deploy-serverless-applications-to-aws-with-apex-up/